#region " LICENSE "
// DRILL - Dependency Resolution Interface Layer Library
// Copyright (c) 2008-2012 Wade A. Reynolds. All rights reserved.
// http://drill.codeplex.com/
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Autofac;
using Autofac.Configuration;
using Drill.Core;
using Drill.Extensibility;

namespace Drill.DrillBits.Autofac
{
    /// <summary>
    /// An implementation of <see cref="IDrillBit"/> based on a Autofac container.
    /// </summary>
    public class AutofacDrillBit : object, IDrillBit
    {
		protected const string AutofacConfigSectionName = "autofac";

		private readonly IDrillBitInitContext _context;
        private IContainer _container;

        /// <summary>
        /// Initializes a new instance of the <see cref="AutofacDrillBit"/> class.
        /// </summary>
		public AutofacDrillBit(IDrillBitInitContext context)
        {
			_context = context;
			Configure();
        }

		/// <summary>
		/// Initializes a new instance of the <see cref="AutofacDrillBit" /> class.
		/// </summary>
		/// <param name="container">The wrapped unity container.</param>
		/// <param name="context">The context.</param>
		/// <exception cref="System.ArgumentNullException">container</exception>
		protected AutofacDrillBit(IContainer container, IDrillBitInitContext context)
        {
            if (null == container)
            {
                throw new ArgumentNullException("container");
            }
            _container = container;
			_context = context;
			Configure();
        }

        /// <summary>
        /// Gets the <see cref="IContainer" /> instance that provides the container implementation.
        /// </summary>
        /// <value>An instance of the <see cref="IContainer" /> interface that provides the container implementation.</value>
        protected IContainer InnerContainer
        {
            get
            {
                CheckDisposed();
                return _container;
            }
        }

        #region " IDependencyResolver Interface Implementation "

		/// <summary>
		/// Gets an instance of the type specified by <paramref name="targetType" />.
		/// </summary>
		/// <param name="targetType">The type of object to be retrieved.</param>
		/// <returns>
		/// An instance of the requested type.
		/// </returns>
		/// <exception cref="System.ArgumentNullException">targetType</exception>
        public object GetInstance(Type targetType)
        {
            CheckDisposed();
			if (null == targetType)
			{
				throw new ArgumentNullException("targetType");
			}
            return _container.Resolve(targetType);
        }

		/// <summary>
		/// Gets a named instance of the type specified by <paramref name="targetType" />.
		/// </summary>
		/// <param name="targetType">The type of object to be retrieved.</param>
		/// <param name="name">The name of the specific object instance.</param>
		/// <returns>
		/// An instance of the requested named type.
		/// </returns>
		/// <exception cref="System.ArgumentNullException">targetType</exception>
        public object GetInstance(Type targetType, string name)
        {
            CheckDisposed();
			if (null == targetType)
			{
				throw new ArgumentNullException("targetType");
			}
            return _container.ResolveNamed(name, targetType);
        }

		/// <summary>
		/// Get all instances of the type specified by <paramref name="targetType" />.
		/// </summary>
		/// <param name="targetType">The type of object to be retrieved.</param>
		/// <returns>
		/// An <see cref="IEnumerable" /> instance representing all
		/// available instances of the requested type.
		/// </returns>
		/// <exception cref="System.ArgumentNullException">targetType</exception>
        public IEnumerable GetAllInstances(Type targetType)
        {
			CheckDisposed();
			if (null == targetType)
			{
				throw new ArgumentNullException("targetType");
			}
	        Type genericIEnumberableType = typeof (IEnumerable<>);
	        Type typeToResolve = genericIEnumberableType.MakeGenericType(new[] {targetType});
			return (IEnumerable)_container.Resolve(typeToResolve);
        }

        /// <summary>
        /// Gets an instance of the type specified by <typeparamref name="TTargetType"/>.
        /// </summary>
        /// <typeparam name="TTargetType">The type of object to be retrieved.</typeparam>
        /// <returns>An instance of the requested type.</returns>
        public TTargetType GetInstance<TTargetType>()
        {
            CheckDisposed();
            return _container.Resolve<TTargetType>();
        }

        /// <summary>
        /// Gets a named instance of the type specified by <typeparamref name="TTargetType"/>.
        /// </summary>
        /// <typeparam name="TTargetType">The type of object to be retrieved.</typeparam>
        /// <param name="name">Name the object was registered with.</param>
        /// <returns>An instance of the requested named type.</returns>
        public TTargetType GetInstance<TTargetType>(string name)
        {
            CheckDisposed();
            return _container.ResolveNamed<TTargetType>(name);
        }

        /// <summary>
        /// Get all instances of the type specified by <typeparamref name="TTargetType"/>.
        /// </summary>
        /// <typeparam name="TTargetType">The type of object to be retrieved.</typeparam>
        /// <returns>
        /// An <see cref="IEnumerable{TTargetType}"/> instance representing all 
        /// available instances of the requested type.
        /// </returns>
        public IEnumerable<TTargetType> GetAllInstances<TTargetType>()
        {
            CheckDisposed();
            return _container.Resolve<IEnumerable<TTargetType>>();
        }

        #endregion " IDependencyResolver Interface Implementation "

        #region " IDrillBit Interface Implementation "

		/// <summary>
		/// Gets the object the DrillBit uses to resolve types.
		/// </summary>
		/// <value>
		/// The object the DrillBit uses to resolve types.
		/// </value>
		/// <remarks>
		/// The resolver object is usually the container object when the DrillBit wraps a DI container.
		/// It may also some other type of object such as a service locator object or any other type of object imaginable.
		/// </remarks>
        public object ResolverObject
        {
            get
            {
				CheckDisposed();
	            return _container;
            }
        }

        #endregion " IDrillBit Interface Implementation "

        #region " IDisposable Interface and Disposable Pattern Implementation "

        /// <summary>
        /// Gets a value indicating whether this instance is disposed.
        /// </summary>
        /// <value><c>true</c> if this instance is disposed; otherwise, <c>false</c>.</value>
        public bool IsDisposed { get; private set; }

        /// <summary>
        /// Performs managed and unmanaged resource cleanup.
        /// </summary>
        public void Dispose()
        {
            // NOTE: The Dispose() method is public.
            // NOTE: The Dispose() method is not virtual because it should not be overridden.

            if (false == IsDisposed)
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
        }

        /// <summary>
        /// Performs managed and unmanaged resource cleanup.
        /// </summary>
        /// <param name="disposing"><c>true</c> if disposing, <c>false</c> if finalizing.</param>
        private void Dispose(bool disposing)
        {
            // NOTE: The Dispose(bool disposing) method is private.
            // NOTE: The Dispose(bool disposing) method is not virtual (because it is private).

            // When the operation is Dispose, call DisposeManagedResources AND DisposeUnmanagedResources.
            // When the operation is Finalize, only call DisposeUnmanagedResources.

            if (false == IsDisposed)
            {
                if (disposing)
                {
                    DisposeManagedResources();
                }

                DisposeUnmanagedResources();

                // Set the _isDisposed flag.
                IsDisposed = true;
            }
        }

        protected virtual void DisposeManagedResources()
        {
            // NOTE: The DisposeManagedResources() method is protected.
            // NOTE: The DisposeManagedResources() method is virtual.

            // Clean up managed resources here.
            // * Call Dispose on all disposable object references
            // * Unsubscribe from all subscribed events
            // * Set managed object references to null

            // Dispose the container.
			if (null != _container)
			{
				_container.Dispose();
			}

            // Release the reference to the container.
            _container = null;
        }

        protected virtual void DisposeUnmanagedResources()
        {
            // NOTE: The DisposeUnmanagedResources() method is protected.
            // NOTE: The DisposeUnmanagedResources() method is virtual.

            // Clean up unmanaged resources here.
            // * Release handles
            // * Free allocated memory
            // * Dereference COM objects
            // * Set large fields to null
        }

        private void CheckDisposed()
        {
            if (IsDisposed)
            {
                string objectName = string.Format("{0}[\"{1}\"]", GetType().Name, _context.Name);

                throw new ObjectDisposedException(objectName, string.Format("{0} has been disposed.", objectName));
            }
        }

        #endregion " IDisposable Interface and Disposable Pattern Implementation "

        private void Configure()
        {
            if (null == _context || null == _context.ConfigurationSources || _context.ConfigurationSources.Count < 1)
            {
                return;
            }

			foreach (IConfigurationSource configurationSource in _context.ConfigurationSources)
            {
                if (null == configurationSource)
                {
                    continue;
                }

                bool isConfigured;
                
                try
                {
                    switch (configurationSource.ConfigurationMethod)
                    {
                        case DrillBitConfigMethod.Default:
                            ConfigureUsingDefaultConfigurationMethod();
                            isConfigured = true;
                            break;

                        case DrillBitConfigMethod.ConfigFile:
                            ConfigureUsingConfigFileConfigurationMethod();
                            isConfigured = true;
                            break;

                        case DrillBitConfigMethod.ConfigFileWithCustomLocation:
							ConfigureUsingConfigFileWithCustomLocationConfigurationMethod(configurationSource);
                            isConfigured = true;
                            break;

						case DrillBitConfigMethod.DrillBitSetup:
							ConfigureUsingSetupProviderConfigurationMethod(configurationSource);
							isConfigured = true;
							break;

                        default:
                            isConfigured = false;
                            break;
                    }
                }
                catch (Exception ex)
                {
					if (ex is DependencyResolverException)
					{
						throw;
					}

					throw
						new AutofacDrillBitException(
							ex
						  , "A unexpected exception occurred while configuring the '{0}' DrillBit "
						  + "using the '{1}' configuration source. "
						  + "See the inner exception for details."
						  , _context.Name
						  , configurationSource.Key
							);
                }

                if (false == isConfigured)
                {
					throw new AutofacDrillBitException(
                        "'{0}' does not support the '{1}' configuration method."
                      , GetType().FullName
                      , configurationSource.ConfigurationMethod
                      );
                }
            }
        }

	    private void ConfigureUsingDefaultConfigurationMethod()
	    {
		    ConfigureUsingConfigFileConfigurationMethod();
	    }

		private void ConfigureUsingConfigFileConfigurationMethod()
		{
			ContainerBuilder builder = new ContainerBuilder();
			try
			{
				builder.RegisterModule(new ConfigurationSettingsReader());
			}
			catch(Exception ex)
			{
				throw new AutofacDrillBitException(
					ex
				  , "An exception occurred while attempting to configure the Autofac container using the default config file. "
				  + "See the inner exception for details."
					);
			}
			_container = builder.Build();
        }

		private void ConfigureUsingConfigFileWithCustomLocationConfigurationMethod(IConfigurationSource configurationSource)
        {
			string filename = configurationSource.ConfigurationObject as string;

			if (string.IsNullOrWhiteSpace(filename))
            {
				throw new AutofacDrillBitException(
                    "The configuration source value for the '{0}' configuration method of the '{1}' "
				  + "dependency resolver is null or empty."
                  , configurationSource.ConfigurationMethod
                  , _context.Name
					);
            }

            if (false == File.Exists(filename))
            {
				throw new AutofacDrillBitException(
                    "The configuration file '{0}' could not be found for the '{1}' "
				  + "configuration method of the '{2}' dependency resolver."
				  , filename
                  , configurationSource.ConfigurationMethod
                  , _context.Name
					);
            }

			ContainerBuilder builder = new ContainerBuilder();
			try
			{
				builder.RegisterModule(new ConfigurationSettingsReader(AutofacConfigSectionName, filename));
			}
			catch (Exception ex)
			{
				throw new AutofacDrillBitException(
					ex
				  , "An exception occurred while attempting to configure the Autofac container using the config file '{0}'. "
				  + "See the inner exception for details."
				  , filename
					);
			}
			_container = builder.Build();
        }

		private void ConfigureUsingSetupProviderConfigurationMethod(IConfigurationSource configurationSource)
		{
			IDrillBitSetup configurationProvider =
				configurationSource.ConfigurationObject as IDrillBitSetup;

			if (null == configurationProvider)
			{
				throw new AutofacDrillBitException(
					"The IDrillBitSetup instance was null for the '{0}' "
				  + "configuration method of the '{1}' dependency resolver. "
				  + "The dependency resolver cannot be configured."
				  , configurationSource.ConfigurationMethod
				  , _context.Name
				  );
			}

			ContainerBuilder builder = new ContainerBuilder();
			try
			{
				configurationProvider.Configure(this, builder);
			}
			catch (Exception ex)
			{
				throw new AutofacDrillBitException(
					ex
				  , "An exception occurred while attempting to configure the Autofac container using the "
				  + "DrillBit setup '{0}'."
				  + "See the inner exception for details."
				  , configurationProvider.GetType().FullName
					);
			}

			// The DrillBit setup for autofac should have already set the container.
			// Check to ensure this has occurred. If not, build and set the container.
			if (null == _container)
			{
				IContainer container;

				try
				{
					container = builder.Build();
				}
				catch (Exception ex)
				{
					throw new AutofacDrillBitException(
						ex
					  , "An exception occurred while attempting to build the Autofac container using the "
					  + "DrillBit setup '{0}'."
					  + "See the inner exception for details."
					  , typeof(AutofacDrillBitSetup).FullName
						);
				}

				SetContainer(container);
			}
		}

	    internal void SetContainer(IContainer container)
	    {
		    _container = container;
	    }
    }
}